package app

import (
	"fmt"
	"os"

	"gitee.com/youkelike/ziam/zoptions"
	"gitee.com/youkelike/ziam/zutils/version"
	log "gitee.com/youkelike/zlog"

	errors "gitee.com/youkelike/ziam/zerrors"
	"github.com/fatih/color"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type RunFunc func(basename string) error

type App struct {
	basename    string // 命令二进制文件名
	name        string // 命令简短描述
	description string
	options     CliOptions
	runFunc     RunFunc
	silence     bool
	noVersion   bool
	noConfig    bool
	// commands    []*Command
	args cobra.PositionalArgs
	cmd  *cobra.Command
}

type Option func(*App)

func WithOptions(opts CliOptions) Option {
	return func(a *App) {
		a.options = opts
	}
}

func WithRunFunc(runFunc RunFunc) Option {
	return func(a *App) {
		a.runFunc = runFunc
	}
}

func WithDescription(desc string) Option {
	return func(a *App) {
		a.description = desc
	}
}

func WithSilence() Option {
	return func(a *App) {
		a.silence = true
	}
}

func WithNoConfig() Option {
	return func(a *App) {
		a.noConfig = true
	}
}

func WithNoVersion() Option {
	return func(a *App) {
		a.noVersion = true
	}
}

func WithValidArgs(args cobra.PositionalArgs) Option {
	return func(a *App) {
		a.args = args
	}
}

func WithDefaultValidArgs() Option {
	return func(a *App) {
		// 设置 root 命令的位置参数验证方法
		a.args = func(cmd *cobra.Command, args []string) error {
			// 不知道传入的参数 args 是一个长度为 0 的切片还是 nil，
			// 下面这样判断最稳妥
			for _, arg := range args {
				if len(arg) > 0 {
					return fmt.Errorf("%q does not take any arguments, but get %q", cmd.CommandPath(), args)
				}
			}
			return nil
		}
	}
}

// name 是简短描述，basename 是命令的二进制文件名
func NewApp(name, basename string, opts ...Option) *App {
	a := &App{
		name:     name,
		basename: basename,
	}
	for _, o := range opts {
		o(a)
	}

	a.buildCommand()
	return a
}

func (a *App) buildCommand() {
	// pflag 全局配置
	initFlags()

	// 创建命令
	cmd := cobra.Command{
		Use:           FormatBasename(a.basename),
		Short:         a.name,
		Long:          a.description,
		SilenceUsage:  true,
		SilenceErrors: true,
		Args:          a.args,
	}
	cmd.SetOut(os.Stdout)
	cmd.SetErr(os.Stderr)
	cmd.Flags().SortFlags = true

	// 串联子命令
	// if len(a.commands) > 0 {
	// 	for _, command := range a.commands {
	// 		cmd.AddCommand(command.cobraCommand())
	// 	}
	// 	// 添加一个 help 子命令
	// 	cmd.SetHelpCommand(helpCommand(a.name))
	// }

	// 设置命令行启动后要执行的任务
	if a.runFunc != nil {
		cmd.RunE = a.runCommand
	}

	// 把所有选项设置都添加到 cobra 对象上，
	// NamedFlagSets 是为了给 FlagSet 分组，在查看命令帮助的时候也能分组展示
	var namedFlagSets zoptions.NamedFlagSets
	if a.options != nil {
		// 框架用一个 CliOptions 接口来与具体配置细节解耦，
		// 有多少组配置，每组配置里有什么，这里都不关心，
		// 只调用 Flags() 就把所有的配置全捞出来了
		namedFlagSets = a.options.Flags()

		// 把 cobra 和 pflag 两处配置组合到一起，
		// **外部 pflag 必须合并到 cobra 才能生效**
		fs := cmd.Flags()
		for _, f := range namedFlagSets.FlagSets {
			fs.AddFlagSet(f)
		}
	}

	// 往名为 global 的 flagset 上添加版本号选项
	if !a.noVersion {
		version.AddVersionFlag(namedFlagSets.FlagSet("global"))
	}
	if !a.noConfig {
		// 1、从配置文件加载选项，在触发 cobra.OnInitialize 事件时才执行
		AddConfigFlag(a.basename, namedFlagSets.FlagSet("global"))
	}
	// 跟 help 对应的选项
	AddHelpFlag(cmd.Name(), namedFlagSets.FlagSet("global"))

	// 设置帮助和示例
	AddCmdTemplate(&cmd, namedFlagSets)

	a.cmd = &cmd
}

// func (a *App) AddCommand(cmd *Command) {
// 	a.commands = append(a.commands, cmd)
// }

// func (a *App) AddCommands(cmds ...*Command) {
// 	a.commands = append(a.commands, cmds...)
// }

// 启动命令行
func (a *App) Run() {
	if err := a.cmd.Execute(); err != nil {
		fmt.Printf("%v %v", color.RedString("Error:"), err)
		os.Exit(1)
	}
}

// 启动命令行后执行的任务
func (a *App) runCommand(cmd *cobra.Command, args []string) error {
	printWorkingDir()
	PrintFlags(cmd.Flags())

	if !a.noVersion {
		// 检查是否在命令行提供 --version 选项，打印版本信息并退出
		version.PrintAndExitIfReqested()
	}

	if !a.noConfig {
		// 2、用命令行中的参数值覆盖配置文件中的同名参数值
		if err := viper.BindPFlags(cmd.Flags()); err != nil {
			return err
		}
		// 3、用合并后的参数值回写到 options，
		// **底层用的是 mapstructure**
		if err := viper.Unmarshal(a.options); err != nil {
			return err
		}
	}

	if !a.silence {
		log.Infow("%v starting %s ...", progressMessage, a.name)
		if !a.noVersion {
			log.Infow("%v version: `%s`", progressMessage, version.Get().ToJSON())
		}
		if !a.noConfig {
			log.Infow("%v config file used: `%s`", progressMessage, viper.ConfigFileUsed())
		}
	}

	if a.options != nil {
		// 执行配置参数补全、验证
		if err := a.applyOptionRules(); err != nil {
			return err
		}
	}

	if a.runFunc != nil {
		return a.runFunc(a.basename)
	}

	return nil
}

func (a *App) applyOptionRules() error {
	if completeableOptions, ok := a.options.(CompleteableOptions); ok {
		if err := completeableOptions.Complete(); err != nil {
			return err
		}
	}

	if errs := a.options.Validate(); len(errs) != 0 {
		return errors.NewAggregate(errs)
	}

	if printableOptions, ok := a.options.(PrintableOptions); ok && !a.silence {
		log.Infow("%v config: `%s`", progressMessage, printableOptions.String())
	}

	return nil
}

var progressMessage = color.GreenString("==>")

func printWorkingDir() {
	wd, _ := os.Getwd()
	log.Infow("%v WorkingDir: %s", progressMessage, wd)
}
